package ${(projectConfig.projectPackageName)!}.common.exception;

import cn.aezo.utils.base.ExceptionU;
import cn.aezo.utils.base.JsonU;
import cn.aezo.utils.base.MiscU;
import cn.aezo.utils.base.StringU;
import ${(projectConfig.projectPackageName)!}.base.Result;
import ${(projectConfig.projectPackageName)!}.common.config.rawbean.CustomObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
import java.util.Map;

@RestController
@ControllerAdvice
public class GlobalExceptionHandlerController extends BasicErrorController {
    private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandlerController.class);

    @Autowired
    private MessageSource messageSource;

    @Autowired
    private Environment env;

    @Autowired
    private CustomObjectMapper customObjectMapper;

    public GlobalExceptionHandlerController() {
        super(new DefaultErrorAttributes(), new ErrorProperties());
    }

    // 错误映射为json，Accept-Type为application/json的
    @Override
    @RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        // TODO 404
        logger.warn(this.getErrorAttributes(request, true).toString());

        Map<String, Object> body = MiscU.Instance.toMap();
        HttpStatus status = getStatus(request);

        try {
            ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
            Throwable throwable = getError(requestAttributes);
            if(throwable == null) {
                throwable = new ExceptionU.UnknownException("Throwable Capture Failed");
                status = HttpStatus.INTERNAL_SERVER_ERROR;
            }

            Result result = this.exception(throwable);
            String str = customObjectMapper.writeValueAsString(result);
            body = JsonU.json2map(str);
        } catch (Exception e) {
            logger.error("Failed to return error message", e);
        }

        return new ResponseEntity<>(body, status);
    }

    // 错误映射到Html，Accept-Type为text/html的
    @Override
    @RequestMapping(produces = {"text/html"})
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        return super.errorHtml(request, response);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public Result exception(Throwable e) {
        return getExceptionResponse(ErrorType.EXCEPTION_ERROR, e);
    }

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ExceptionU.AuthLoginFailedException.class)
    public Result authLoginFailedException(Throwable e) {
        return getExceptionResponse(ErrorType.AUTH_LOGIN_FAILED_ERROR, e);
    }

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ExceptionU.AuthTokenInvalidException.class)
    public Result authTokenInvalidException(Throwable e) {
        return getExceptionResponse(ErrorType.AUTH_TOKEN_INVALID_ERROR, e);
    }

    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    @ExceptionHandler(ExceptionU.MethodNotSupportedException.class)
    public Result httpRequestMethodNotSupportedException(Throwable e) {
        return getExceptionResponse(ErrorType.METHOD_NOT_SUPPORTED_ERROR, e);
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ExceptionU.ArgumentMissingException.class)
    public Result missingServletRequestParameterException(Throwable e) {
        return getExceptionResponse(ErrorType.ARGUMENT_MISSING_ERROR, e);
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ExceptionU.ArgumentParseException.class)
    public Result httpMessageNotReadableException(Throwable e) {
        return getExceptionResponse(ErrorType.ARGUMENT_PARSE_ERROR, e);
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ExceptionU.ArgumentInvalidException.class)
    public Result methodArgumentNotValidException(Throwable e) {
        return getExceptionResponse(ErrorType.ARGUMENT_INVALID_ERROR, e);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(ExceptionU.UnknownException.class)
    public Result unknownException(Throwable e) {
        return getExceptionResponse(ErrorType.UNKNOWN_EXCEPTION_ERROR, e);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(ExceptionU.TodoException.class)
    public Result todoException(Throwable e) {
        return getExceptionResponse(ErrorType.TODO_ERROR, e);
    }

    private Result getExceptionResponse(ErrorType errorType, Throwable e) {
        ExceptionInfo exceptionInfo = new ExceptionInfo();

        String localMessage = getLocalMessage(errorType);
        String exceptionMessage = e.getMessage();
        StackTraceElement[] stackTrace = e.getStackTrace();

        String[] actives = env.getActiveProfiles();
        if (actives == null || actives.length <= 0 || !"prod".equals(actives[0])) {
            if (StringUtils.isNotEmpty(localMessage)) {
                exceptionInfo.setLocalMessage(localMessage);
            }
            if (StringUtils.isNotEmpty(exceptionMessage)) {
                exceptionInfo.setExceptionMessage(exceptionMessage);
            }
            if (stackTrace != null) {
                exceptionInfo.setStackTrace(stackTrace);
            }
        } else if (e instanceof ExceptionU) {
            if (StringUtils.isNotEmpty(exceptionMessage)) {
                exceptionInfo.setExceptionMessage(exceptionMessage);
            }
        }

        logger.error(StringU.buffer(", ", errorType.getErrorCode(), errorType.getMessage(), localMessage), e);

        return Result.failure(errorType.getMessage(), exceptionInfo);
    }

    private String getLocalMessage(ErrorType errorType) {
        String localMessage = null;
        Locale locale = null;
        try {
            locale = LocaleContextHolder.getLocale();
            localMessage = messageSource.getMessage(errorType.getErrorCode(), null, locale);
        } catch (NoSuchMessageException e1) {
            logger.warn("invalid i18n! errorCode: " + errorType.getErrorCode() + ", local: " + locale);
        }

        return localMessage;
    }

    public Throwable getError(RequestAttributes requestAttributes) {
        Throwable exception = (Throwable) requestAttributes.getAttribute(DefaultErrorAttributes.class.getName() + ".ERROR", 0);
        if(exception == null) {
            exception = (Throwable) requestAttributes.getAttribute("javax.servlet.error.exception", 0);
        }

        return exception;
    }

    public class ExceptionInfo {
        private String localMessage;

        private String exceptionMessage;

        private StackTraceElement[] stackTrace;

        public String getLocalMessage() {
            return localMessage;
        }

        public void setLocalMessage(String localMessage) {
            this.localMessage = localMessage;
        }

        public String getExceptionMessage() {
            return exceptionMessage;
        }

        public void setExceptionMessage(String exceptionMessage) {
            this.exceptionMessage = exceptionMessage;
        }

        public StackTraceElement[] getStackTrace() {
            return stackTrace;
        }

        public void setStackTrace(StackTraceElement[] stackTrace) {
            this.stackTrace = stackTrace;
        }
    }
}